GNU的obj分析工具的使用 - nm,objdump

linux GUN工具链中二进制文件分析工具:

  • nm/objdump用来列出目标文件(object files)的符号表(symbols);
  • readelf用来分析elf文件;
  • ldd用来分析程序运行时需要依赖的动态库。

先来回顾一下Linux进程内存布局:

----高地址----
<栈stack>
|
<堆heap>
<.bss> 未初始化的全局变量
<.data> 已初始化的全局变量,static变量
<.rodata> 属于.data, 存放const, char*字符串
<.txt> 代码段
----低地址----

本文中使用的示例代码:

#include <stdio.h>

char *global_string1 = "abc"; // 4字节
char *global_string2 = "Hello World!!!"; // 15字节
const int global_const_int = 0x81; // 129
int global_int = 0x3FF; // 1023
int global_uninit_int;

int main()
{
int stack_int = 0x1F; // 31
static int stack_uninit_static_int;
static int stack_inited_static_int = 0x1B; // 27

char *stack_string1 = "Hello World!!!"; // 同global_string2, 15字节
char *stack_string2 = "Hello"; // 6字节

// 先看一下环境是多少位
printf("sizeof int is %d\n", sizeof(int));

// 打印地址, 从低地址到高地址:
// 全局定义字符串和函数内定义的字符串:
printf("addr of global_string1 is 0x%x\n \
addr of global_string2 is 0x%x\n \
addr of stack_string1 is 0x%x\n \
addr of stack_string2 is 0x%x\n", global_string1, global_string2, stack_string1, stack_string2);

// 全局const常量
printf("addr of global_const_int is 0x%x\n", &global_const_int);

// 已初始化static变量(全局变量默认是static的, 以及函数内static变量)
printf("addr of global_int is 0x%x\n \
addr of stack_inited_static_int is 0x%x\n", &global_int, &stack_inited_static_int);

// static但未初始化变量(全局的和函数内的)
printf("addr of global_uninit_int is 0x%x\n \
addr of stack_uninit_static_int is 0x%x\n", &global_uninit_int, &stack_uninit_static_int);

// 栈
printf("addr of stack_int is 0x%x\n", &stack_int);

return 0;
}

编译: gcc -g test.c -o test && ./test ,程序的输出如下, #后面是我的注释:

sizeof int is 4
addr of global_string1 is 0x9343df2 # 4字节的字符串
addr of global_string2 is 0x9343df6 # 15字节的字符串
addr of stack_string1 is 0x9343df6 # 函数内字符串"Hello World!!!", 跟全局指向同一个
addr of stack_string2 is 0x9343e05 # global_string2后面15字节就是stack_string2

addr of global_const_int is 0x9343f94 # 全局常量在字面量字符串更高一点的位置

addr of global_int is 0x9344028 # 全局/局部的static变量
addr of stack_inited_static_int is 0x934402c

addr of global_uninit_int is 0x9344034 # 未初始化static
addr of stack_uninit_static_int is 0x9344030

addr of stack_int is 0x568bc598

“全局变量”默认是static的, 无论加不加static关键字, 全局变量存储在data区
“局部定义的static变量”, 跟全局变量都在data区

在内存的布局从大约是(非相邻的有|隔开了, 没有|表示相邻的区域):

-- 高地址 --
---- [栈区] -----
函数内定义的非static变量(栈变量)
|
|
|
----- [BSS] -----
未初始化static变量(包括全局定义和函数内定义的)
---- [data区] ----
全局非const变量
-- [data.rodata区]--
全局const常量
字面量的字符串(包括"全局区定义的"以及"在函数内定义的")
-- 低地址 --

注意:全局字符串char*类型和char[]类型是有区别的,前者的字符不允许被修改,而后者的字符可以被修改。 未初始化的栈变量其值是随机的。而未初始化的全局变量被放入.bss段,被初始化为zero。

Wiki: Data segment:
The BSS segment ( Block Started by Symbol), also known as uninitialized data, is usually adjacent to the data segment and contains all global variables and static variables that are initialized to zero or do not have explicit initialization in source code. For instance a variable declared static int i; would be contained in the BSS segment.
The data area contains global and static variables used by the program that are explicitly initialized with a non-zero (or non-NULL) value.

nm的使用

nm用来列出目标文件的符号(symbol)清单: 在当前目录下输入nm hello,返回如下:

0000000000601054 B __bss_start
0000000000601054 b completed.6972
000000000040063c R const_num // 全局const常量,注意地址
0000000000601030 D __data_start
0000000000601030 W data_start
...
0000000000601054 D _edata
0000000000601060 B _end
...
0000000000601050 D global_num //全局int变量,注意地址
0000000000601000 d _GLOBAL_OFFSET_TABLE_
0000000000601040 D global_string //全局char*字符串,注意地址
0000000000601048 D global_string2 //全局charp[]字符串
0000000000601058 B global_uninit_num // 全局未初始化
...
000000000040052d T main
U printf@@GLIBC_2.2.5
00000000004004a0 t register_tm_clones
0000000000400440 T _start
0000000000601058 D __TMC_END__

解释下nm返回的格式, 共3列,分别是”符号在文件里的偏移”,”符号的类型”,”符号名称”.
其中“符号类型”有下面几种:

A :该符号的值是绝对的,在以后的链接过程中,不允许进行改变。
B :该符号的值出现在非初始化数据段(.bss)中。例如,比如全局没初始值的变量global_uninit_num;
D :该符号放在普通的数据段(.data)中,通常是那些已经初始化的全局变量;
R :The symbol is in a read only data section,比如全局的const_num;
T :该符号放在代码段中,通常是那些全局非静态函数, 上面可以看到main/_start等都是T类型;
U :该符号未定义过,需要自其他对象文件中链接进来, 上面可以看到printf函数是 printf@@GLIBC;

程序打印的变量内存地址(运行时),和可执行文件符号表的地址,并不完全相同。比如全局字符串global_string,(和全局int相比)全局字符串在程序运行时会放到.data段更低的位置。详细的解释见后面readelf的说明。

然后回到上面的一个问题,为什么全局字符串char *global_string = "abc"和全局常量const int const_num =128在内存中的地址比全局变量int global_num = 1024的地址要低很多? 并且看到上面nm分析的obj符号地址,全局变量/常量在符号表里的地址其实差不多。
原因(我猜的)是,编译器在链接时会对“不可改变的”常量做特殊的优化,比如上面的char*类型的字符串,把这些不可改变的常量(.rodata段)存放在代码段(.text段),防止意外改写。

nm的常用参数:

-C : 加上此参数, 会让符号变成”适合阅读”的样式;
-A 在每个符号信息的前面打印所在对象文件名称;
-l 使用对象文件中的调试信息打印出所在源文件及行号, gcc -g参数可以让打印更为详尽;

nm可以用来:

  1. 判断指定程序中有没有某个符号 (比较常用的方式:nm -C a.out | grep symbol)
  2. 解决程序编译时undefined reference的错误,以及mutiple definition的错误
  3. 查看某个符号的地址,以及在进程空间的大概位置(bss, data, text区,具体可以通过第二列的类型来判断)

有关nm更详细的说明可以参考 @Ref sourceware.org

objdump的使用

objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,可以看作是nm的增强型。
objdump -d out :反汇编test中的需要执行指令的那些section;
objdump -x out :以某种分类信息的形式把目标文件的数据组成输出;
objdump -t out :输出目标文件的符号表;
objdump -h out :输出目标文件的所有段概括;
objdump -j ./text/.data -S out : 输出指定段的信息(反汇编源代码);
objdump -S out :输出目标文件的符号表() 当gcc -g时打印更明显;
objdump -j .text -Sl stack1 | more:
-S 尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。
-l 用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用。使用-ld和使用-d的区别不是很大,在源码级调试的时候有用,要求编译时使用了-g之类的调试编译选项。

readelf的使用

objdumpreadelf都可以用来查看二进制文件的一些内部信息. 区别在于:

  • objdump借助BFD而更加通用一些, 可以应付不同文件格式
  • readelf则并不借助BFD, 而是直接读取ELF格式文件的信息, 按readelf手册页上所说,得到的信息也略细致一些.

readelf可以很方便的查看elf文件的布局: readelf -ahW hello

详细的介绍请参考, 这里就不再复制粘贴了-.-
@Ref readelf - GNU Binary Utilities

参数-a表述输出所有elf信息, h表示打印出elf head, W表示打印出的内容太长(>80字)不换行, 方便查看;

readelf -ahW test 打印出:

[Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
[11] .init PROGBITS 00000000004003e0 0003e0 00001a 00 AX 0 0 4
[12] .plt PROGBITS 0000000000400400 000400 000040 10 AX 0 0 16
[13] .text PROGBITS 0000000000400440 000440 0001e2 00 AX 0 0 16
[14] .fini PROGBITS 0000000000400624 000624 000009 00 AX 0 0 4
[15] .rodata PROGBITS 0000000000400630 000630 000165 00 A 0 0 8
[24] .data PROGBITS 0000000000601030 001030 000024 00 WA 0 0 8
[25] .bss NOBITS 0000000000601054 001054 00000c 00 WA 0 0 4
[26] .comment PROGBITS 0000000000000000 001054 000024 01 MS 0 0 1

Key to Flags:

W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

根据上面的打印, 第三列就是elf文件中每段的起始address, 可以看到分别是(地址低-高):

.text: 0x400440,
.rodata: 0x400630,
.data(已初始化的全局和static变量): 0x601030,
.bss(未初始化的全局):0x601054

  • “.text”段即为代码段,是存储指令的段,为防止在运行过程中指令被修改,该段是只读的
  • “.bss段”:未初始化的全局变量。在目标文件中这个段不占据实际的空间,在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。从上面的 readelf输出可以看到,“.data”和“.bss”在加载时合并到一个Segment中,这个Segment是可读可写的。“.bss段”和“.data段”的不同之处在于,.bss段在文件中不占存储空间,在加载时这个段用0填充。

为什么未初始化的数据称为.bss?
用术语.bss来表示未初始化的数据是很普遍的。它起始于IBM704汇编语言中的“块存储开始(Block Storage Start)”指令的首字母缩写,并沿用至今,一个记住区分.data和.bss节的简单方法是把“bss”看成是“更好地节省空间(Better Save Space)”的缩写。

Ok, 我们再来看一下readelf -ahW的其他输出:

50: 000000000040064c 4 OBJECT GLOBAL DEFAULT 15 const_num
64: 0000000000601048 8 OBJECT GLOBAL DEFAULT 24 global_string2
65: 0000000000601040 8 OBJECT GLOBAL DEFAULT 24 global_string

可以看到代码里的int const const_num在0x40064c这个地址, 正好处在.rodata区域, 我们用hexdump命令来查看这个段的内容, 发现”hello world”字符串也在这个区域. 在链接时,“.rodata”和“.text”合并到Text Segment中,在加载运行时,操作系统将Text Segment设为只读保存起来,防止意外改写。
需要注意的是,象const int A这样的变量在定义时必须进行初始化,因为只有初始化时才有机会给它一个值,一旦定义之后就不能再改写了,也就是不能再赋值了。

ldd的使用

ldd工具用来查看程序所依赖的动态库,在命令行输入 ldd hello

linux-vdso.so.1 =>  (0x00007fff36450000)
libc.so.6 => /lib64/libc.so.6 (0x000000355d800000)
/lib64/ld-linux-x86-64.so.2 (0x000000355d000000)

说明: 上面最后一列十六进制数, 就是库加载的开始地址.

@Ref 参考 http://akaedu.github.io/post/13/13.1.html